package kuti.grp5;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CyclicBarrier;
import java.util.logging.Level;
import java.util.logging.Logger;
import kuti.*;

/**
 * This class it's the core of the simulation, here we instantiate all the
 * others managers, and we prepare, start and handle the simulation.
 *
 * @author Marc Schorderet <marc.schorderet@edu.hefr.ch>
 * @author Vincenzo Do <vincenzo.do@edu.hefr.ch>
 *
 */
public class WorldManager extends Thread implements IWorldManager {

    private ILawn lawn;
    private long stepNr;
    private ISimulParams simulParams;
    private double score;
    private List<IKuti> kutiList;
    private List<IObstacle> obstacleList;
    private List<IChargingStation> chargingStationList;
    private List<IWorldObserver> changeObserverList;
    private List<ILawnArea> changeLawn;
    private CyclicBarrier barrier;
    private CopyOnWriteArrayList<IKuti> activeKutiList;
    private ScoreManager scoreManager;
    private BarrierManager barrierManager;

    /**
     * Constructor for the class WorldManager, instantiate the used variables.
     *
     * @param simulParams
     * @param lawn
     */
    public WorldManager(ISimulParams simulParams, ILawn lawn) {
        this.lawn = lawn;
        this.simulParams = simulParams;
        kutiList = new ArrayList();
        obstacleList = new ArrayList();
        chargingStationList = new ArrayList();
        changeObserverList = new ArrayList();
        changeLawn = new ArrayList();
        stepNr = 0;
        score = 1.0;
        activeKutiList = new CopyOnWriteArrayList();
        scoreManager = new ScoreManager(lawn.getBladeHeight(), simulParams);
    }

    /**
     * This method separates in different list the objects for the simulation We
     * have a list for each kind of elements: IKuti, IObstacle and
     * IMagneticObstacle, IChargingStation.
     *
     * PRE: has to be called before startSimulation()
     *
     * @param list list of all elements for a simulation
     * @param isp parameters for a simulation
     */
    @Override
    public void populateWorld(List<IWorldElement> list, ISimulParams isp) {
        simulParams = isp;

        for (IWorldElement e : list) {
            if (e instanceof IKuti) {
                kutiList.add((IKuti) e);
                ((Kuti) e).setWorldManager(this);
            }
            if (e instanceof IObstacle) {
                obstacleList.add((IObstacle) e);
            }
            if (e instanceof IChargingStation) {
                chargingStationList.add((IChargingStation) e);

            }
            //to not grow the lawn on the zone of a chargingStation
            ((LawnArea) lawn.getBladeHeight()).setChargingStationList(chargingStationList);

        }
    }

    /**
     * This method prepares the simulation for the run. For each Kuti, set:
     * lawn, actuator, and put his state to active. For each Actuator set:
     * barrier, Kuti. Create the BarrierManager, and start the drivers thread.
     */
    private void initSimulation() {
        // Initialize barrier
        barrier = new CyclicBarrier(kutiList.size() + 2);
        // Initialize the step number
        stepNr = 0;
        //Init the kuti, actuator and the barrierManager, then start the threads
        for (IKuti kuti : kutiList) {
            ((Kuti) kuti).getCutterManager().setLawn(lawn);
            IDriver driver = kuti.getDriver();
            //Add the kuti in the active list.
            activeKutiList.add(kuti);
            barrierManager = new BarrierManager(barrier, activeKutiList, this);
            IActuator a = new Actuator(driver, kuti, barrierManager.getBarrier());
            ((Kuti) kuti).setActuator(a);
            ((Kuti) kuti).setActive(true);
            driver.startDriverThread(a, kuti, kuti.getRadar(), simulParams);
        }
    }

    /**
     * This method is used to start the simulation. PRE: has to be called after
     * populateWorld()
     */
    @Override
    public void startSimulation() {
        initSimulation();
    }
    

    /**
     * This method manages a tick of the simulation. The steps for each tick
     * are: 1) Incrementation of the tick number 2) Increase the size of the
     * lawn 3) Evaluate the decisions of the drivers 4) Apply the valid
     * decisions 5) Manage the recharge of energy 6) Consumes the required
     * energy for the applied action 7) Update the radar 8) Calculate the score
     *
     * PRE: the simulation has to be started
     */
    @Override
    public void doStep() {


        boolean isSimulationStopped = stepNr >= simulParams.getMaxSimulationSteps();

        // Check if the simulatio has to be stopped
        if (isSimulationStopped) {

            stopSimulation();

        }

        // 1) Increment the step number
        stepNr++;

        //wait on barrier (wait drivers decisions)
        synchronized (this) {
            barrierManager.setActiveKutiList(activeKutiList);
            Thread barrierManagerThread = new Thread(barrierManager);
            barrierManagerThread.start();
            try {
                barrierManager.getBarrier().await();
            } catch (InterruptedException ex) {
                Logger.getLogger(WorldManager.class.getName()).log(Level.SEVERE, null, ex);
            } catch (BrokenBarrierException ex) {
                barrierManager.resetBarrier();
                // TODO capire perchè lancia la brokenBarrier Exception.
            }

            activeKutiList = barrierManager.getActiveKutiList();
        }

        // 2) Increase the lawn size
        if (stepNr % simulParams.getBladeGrowthPeriod() == 0) {
            ((LawnArea) lawn.getBladeHeight()).growBladeInLawn();

            for (IWorldObserver e : changeObserverList) {
                e.globalBladeIncrement();
            }

        }

        // 3),4),5),6),7)
        for (IKuti k : activeKutiList) {

            Actuator actuator = (Actuator) ((Kuti) k).getActuator();
            Kuti kuti = (Kuti) k;

            if (kuti.isActive()) {

                actuator.setColision(false);

                boolean isCutterOn = actuator.getCutterState() == ECutterState.ON;

                // Check for magnetic collisions
                if (kuti.getCollisionManager().getMagneticCollision()) {
                    //Check if the kuti dicided to move
                    if (kuti.getState() == EKutiState.MOVING) {
                        kuti.getEnergyManager().consumeEnergy(simulParams.getMagnetLiberationEnergy());
                        kuti.getCollisionManager().setMagneticCollision(false);
                    }
                }

                // Moving, change orientation, change cutter state, stopping, ...
                kuti.getActionManager().doActionForThisStep(stepNr, changeObserverList);

                if (isCutterOn) {
                    //Tondre le gazon
                    LawnArea lawnArea = kuti.getCutterManager().cutLawn(kuti.getActuator().traveledDistance());
                    changeLawn.add(lawnArea);
                }

            }

        }
        //Update info for the interface
        for (IWorldObserver e : changeObserverList) {
            e.stateChanged(kutiList, changeLawn);
        }

        changeLawn.clear();

        // 8) Calculate the score
        if (stepNr % simulParams.getScoreEvalInterval() == 0 || isSimulationStopped) {
            // Do an update of the score value
            score = scoreManager.getScore();
        }

        //nextStep for kuti
        signalKuti();
    }

    /**
     * This method ends the simulation, stops the drivers thread. PRE: the
     * simulation must be running.
     */
    @Override
    public void stopSimulation() {
        for (IKuti kuti : kutiList) {
            kuti.getDriver().stopDriverThread();
        }
        //TODO deactivate thread from barrier
        // barrierManagerT = null;
    }

    /**
     * This method is used to signal all the Kuti, telling them that they can
     * continue. Called after the WM has evaluated and applied the actions.
     */
    private void signalKuti() {
        for (IKuti k : activeKutiList) {
            Actuator actuator = (Actuator) ((Kuti) k).getActuator();
            actuator.nextStep();
        }

    }

    /**
     * Getter for the lawn.
     *
     * @return lawn of the simulation
     */
    @Override
    public ILawn getLawn() {
        return lawn;
    }

    /**
     * Getter for the list of all the Kuti.
     *
     * @return list with all the Kuti
     */
    @Override
    public List<IKuti> getAllKuti() {
        return kutiList;
    }

    /**
     * Getter for the list of all obstacle.
     *
     * @return list with all the Obstacles
     */
    @Override
    public List<IObstacle> getAllObstacle() {
        return obstacleList;
    }

    /**
     * Getter for the list of all charging stations.
     *
     * @return list with all the chargingStations
     */
    @Override
    public List<IChargingStation> getAllChargingStation() {
        return chargingStationList;
    }

    /**
     * Getter for the parameters of the simulation
     *
     * @return the parameters of the current simulation
     */
    @Override
    public ISimulParams getSimulParams() {
        return simulParams;
    }

    /**
     * Getter for the step number
     *
     * @return the current step number.
     */
    @Override
    public long getStepNr() {
        return stepNr;
    }

    /**
     * Getter for the score
     *
     * @return Returns the score.
     */
    @Override
    public double getScore() {
        return score;
    }

    /**
     * Method to add an Observer
     *
     * @param iwo
     */
    @Override
    public void addChangeObserver(IWorldObserver iwo) {
        changeObserverList.add(iwo);
    }

    /**
     * Method to remove an Observer
     *
     * @param iwo
     */
    @Override
    public void removeChangeObserver(IWorldObserver iwo) {
        changeObserverList.remove(iwo);
    }
}
